docker编排+低代码部署Headscale VPN打通大内网
为什么是VPN?
前面讲过,我企图打通各个住所和学校的内网。列位要问了,你不是搞过FRP内网穿透吗,为啥还要VPN?我个人的理解是:FRP侧重于服务,依托于开放的端口;VPN侧重于互连,依托于C/S架构和IP,对比于下表。可见,要打通各个内网,必须使用基于VPN的技术才行。
对比项 | FRP | VPN |
---|---|---|
开放端口数 | 随服务数增加 | 很少 |
主要应用 | 对外提供服务,网页服务较多 | 对内提供连通 |
穿透方向 | 单向 | 双向(通过路由) |
安全性 | 一般 | 强 |
IP级互连 | 不支持 | 支持 |
额外的客户端 | 不需要 | 一般需要 |
部署难度 | 容易 | 困难 |
用哪个VPN?
关于主流VPN技术,下面这篇文章总结的挺好。
我斗胆再一句话总结下:PPTP
不安全;OpenVPN
针对IPSec/L2TP
做了减法;WireGuard
针对OpenVPN
又做了减法,性能更高,还支持了去中心化。
可见,WireGuard
是目前最先进的VPN技术,已被引入Linux内核,必须选她!
还有个原因,群晖的VPN服务端都被阉割了,自己装套件起OpenVPN
也不行;威联通的OpenVPN
服务端可以,但静态路由设置时总是出错。
为什么是Headscale
?
WireGuard
目前只是一个内核级别的模块,想要配置好裸的WireGuard
,低代码是别想了,那么多对端秘钥,增、删节点都需要改动所有节点的配置,想一想就头疼!
表扬威联通,已经支持图形化界面的WireGuard
服务器和客户端。
基于WireGuard
的上层应用,目前比较成熟的有Tailscale
和Netmaker
。Tailscale
是在用户态实现了 WireGuard
协议;Netmaker
直接使用了内核态的WireGuard
,理论上性能更高,但目前缺乏中继机制(类似FRP),应用场景受限。Headscale
是Tailscale
的开源实现,适合私有部署,就选她了!
本文动机
知乎上介绍Headscale
的很少;找遍全网,也很少有低代码、快速部署Headscale
的文章,能讲清楚原理和为什么这样配置的就更少了。
仍然要感谢一些博主,虽然不讲原理,但内容确实丰富,给我一定启发(其实是偷懒不用去看文档了),比如下面这个。
我在群晖和威联通的NAS上都用docker-compose
部署成功了,必须向大家汇报下,希望能帮助更多非专业领域的“私有云折腾师”。
Headscale搭建
架构介绍
主节点(我自己定义的概念)的网络拓扑如下图所示。其他节点与之类似,不包含服务端及其UI。
服务端(server),又叫协调服务器。负责WireGuard
节点的公钥交换、虚拟IP分配、路由转发的公开和访问控制。
客户端(client),即WireGuard
节点。目前仍然使用的是Tailscale
的开源客户端,采用go
语言编写,在用户空间实现WireGuard
。
中继端(derp),是P2P连接时NAT穿透的保底方案。DERP(Detoured Encrypted Routing Protocol)是Tailscale
自研的协议,运行在 HTTP 之上 ,根据目的公钥来中继加密的流量。中继端同时支持DERP和STUN。
关于NAT穿透的原理,可以参考下面这篇。
可见,服务端负责控制,中继端负责数据通路,客户端发起/接受连接,是可以部署在不同的服务器上的。这里我们资源有限,把他们都部署在一个NAS里,还需要使用反向代理(lucky
)以“零代码”支持带SSL证书的HTTPS访问;为了“低代码”配置服务端,我们给她再加一个服务端控制界面(webui,以下简称UI端),齐活。
关于客户端,其实有两个作用。一是做为WireGuard
的节点连到大内网里。
这时,为了减少路由的层级,其容器的网络类型一般设为host。
二是通过Unix的进程间通信(sock)为中继端提供用户认证,防止中继端被他人使用。
通过把客户端和中继端的/var/run/headscale链接在一起来实现。这时,其容器的网络类型最好设为bridge。
如何选择容器网络类型,可以参考下面的公式。
假设,中继端部署在服务器A上,负责VPN路由的是服务器B。
if(A == B)
在A上部署客户端,容器网络使用host。
else {
在A上部署客户端,容器网络使用bridge或host都行。
在B上部署客户端;如果使用容器,其网络使用host。// 例如,OpenWRT上可以直接部署。
}
关于自定义的容器子网,可以参考下面这篇文章。
我把这些容器都部署在一个NAS上,所以用host。相关的端口如下表,使用了基于子域名的lucky
反向代理后,只需要对公网(别忘了在路由器上做端口映射)暴露一个STUN的UDP3478
端口(新增)和一个lucky
反向代理的端口(例如8080
,已有)。相比FRP,美极了。
服务端 | UI端 | 中继端DERP | 中继端STUN | |
---|---|---|---|---|
端口类型 | TCP | TCP | TCP | UDP |
容器侧端口 | 8080 | 7070 | 6060 | 3478 |
NAS侧端口 | 58080 | 57070 | 56060 | 3478 |
HTTPS反向代理 | 需要 | 需要 | 需要 | 不需要 |
容器编排
直接给出带注释的四合一docker-compose.yaml
,全网罕见。
version: '3.9'
networks: # 定义编排容器的子网
private:
driver: bridge
ipam:
config:
- subnet: 172.18.200.0/24
services:
server: # 服务端
image: headscale/headscale
container_name: headscale-server
networks:
- private
volumes:
- ./headscale/config:/etc/headscale # 提前放好config.yaml和derp.yaml
- ./headscale/data:/var/lib/headscale
- ./headscale/run:/var/run/headscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro # 使用NAS的时间
ports:
- "58080:8080" # listen port
command: serve # v0.22及以前的版本需要使用headscale serve
restart: unless-stopped
depends_on:
- derp
webui: # UI端
image: ghcr.io/gurucomputing/headscale-ui
container_name: headscale-ui
networks:
- private
environment:
HTTP_PORT: 7070
ports:
- "57070:7070"
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
derp: # 中继端
image: fredliang/derper
container_name: headscale-derp
networks:
- private
environment:
DERP_DOMAIN: derp.example.com # 替换为自己的域名
DERP_ADDR: :6060 # 注意,前面有个英文冒号
DERP_CERT_MODE: letsencrypt # 使用了lucky做反向代理,理论上不需要设置,但我还没试过。
DERP_VERIFY_CLIENTS: true # 还用client做认证时,配置为true
ports:
- "56060:6060" # derp port, TCP
- "3478:3478/udp" # STUN port, UDP
volumes:
- ./tailscale:/var/run/tailscale
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
restart: unless-stopped
depends_on:
- client
client: # 客户端
image: tailscale/tailscale
container_name: headscale-client
network_mode: "host" # 用做连接各子网的客户端时,这样最简单
privileged: true
environment:
TS_EXTRA_ARGS: --netfilter-mode = off # 默认不开启路由转发,更灵活
volumes:
- ./tailscale:/var/run/tailscale # 要在NAS上和derp共享同一个目录
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
- /var/lib:/var/lib
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
command: tailscaled
restart: unless-stopped
注意,要提前配置好config.yaml
和derp.yaml
。可以去GitHUB的代码仓,下载config-example.yaml
和derp-example.yaml
,修改好内容(见下文)并重命名。
我用的是latest
映像,当前对应源码的版本是v0.23.0-alpha5
。配置文件如果报错,可以去搜一下Issues
,一般都有答案。
另外,只需要把docker-compose.yaml
中server
和webui
的部分注释掉,就可以部署在其他节点。如果不想增加中继端,也可以把derp
的部分注释掉。
服务端配置
config.yaml
中修改的地方如下。
server_url
要改成反向代理后的网址。- 把
urls
下面的网址注释掉,不使用官方的中继端。 - 增加
derp.yaml
的位置,指定自己搭建的中继端。 - 注意各端口要和
docker-compose.yaml
中的对应。
server_url: https://headscale.example.com:8080
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics, you may want to keep this endpoint private to your internal network
metrics_listen_addr: 0.0.0.0:9090
grpc_listen_addr: 0.0.0.0:50443 # 看起来没啥用
ip_prefixes:
100.100.0.0/16
# List of externally available DERP maps encoded in JSON
urls:
#- https://controlplane.tailscale.com/derpmap/default
# Locally available DERP map files encoded in YAML
paths:
- /etc/headscale/derp.yaml
derp.yaml
如下,这里我添加了两个中继端。
regions:
900:
regionid: 900
regioncode: hz
regionname: China Telecom Hangzhou
nodes:
- name: shelter1-derp
regionid: 900
hostname: derp.example.com
stunport: 3478
stunonly: false
derpport: 8080
- name: shelter2-derp
regionid: 900
hostname: derp.mirror.example.com
stunport: 3478
stunonly: false
derpport: 8080
反向代理配置
服务端和中继端的反向代理没什么可说的,唯有UI端有点特殊。
headscale-ui
这个项目原始的计划是和服务端部署在同一个服务器里,例如:服务端通过协议://域名:端口号
访问,UI端就必须通过协议://域名:端口号/web
访问。其原理是UI端调用服务端的HTTP的API(需要配置API Key)来操作服务端,这就带来一个问题:使用容器部署时,不能随意设置反向代理,否则会有跨域访问被拒绝的问题。
必须保证服务端和UI端使用相同的域名、相同的端口号和相同的协议(HTTPS/HTTP)。因此,很多教程去介绍如何用Nginx
、Caddy
和Traefik
等反向代理工具的配置,需要写大量代码,要弄懂得学习HTTP传输原理。我在lucky
里探索出一个非常简单的方法,零代码解决,必须分享给大家。
关于lucky
反向代理,可以参考下面这篇文章。
首先,服务端和UI端使用相同的代理端口,例如8080
。如果服务端的“前端域名/地址”是headscale.example.com
,UI端的“前端域名/地址”就填headscale.example.com/web
,UI端的“后端地址”就填NAS的IP地址:57070(UI的NAS侧端口号)/web
。这样,访问https://headscale.example.com:8080/web
就能访问到UI端且后续操作不会产生跨域问题。
这时,服务端的URL是https://headscale.example.com:8080
。
服务端操作(步骤1)
其实服务端支持很多命令行操作,但我们追求“低代码”,只需要用命令行生成一个API Key,剩下的工作在UI端点鼠标就行了。
进入容器,执行命令,把生成的API Key记录下来:
$ headscale apikeys create -e 9999d
其中,-e
后面指定的是过期时间,这里我指定9999天,27年后看能否有人攻破。
也可以在宿主机上执行,前面加sudo docker exec -it
即可,不会的可以练练。
UI端操作(步骤2)
- 打开UI的URL,本例为
https://headscale.example.com:8080/web
。
2. 进入“Settings”。
3. 添加“Headscale URL”,本例为https://headscale.example.com:8080
。
4. 把服务端生成的Key添加到“Headscale API Key”。
5. 点击“Test Server Settings”,出现绿色对号后UI端就可以接管服务端了,如下图所示。
6. 进入“User View”,点击“+New User”,添加一个用户。
7. 为该用户生成一个Preauth Key,供客户端连接使用。为了便捷性,最好设置为“Reusable”,并“Active”,如下图。
连接的密钥设置比较灵活,有两种方法。一种是上面这种:在服务端生成Preauth Key(1个共享或多个独立),客户端连接时指定,成功后在“Device View”里就能看到各个节点。另一种是在客户端连接时生成,在UI端的“Device View”里手动添加秘钥、注册节点。我这么懒惰,当然共享1个Preauth Key。
客户端操作(步骤3)
- 进入各客户端的容器,执行命令。
$ tailscale up --netfilter-mode=off \
--accept-routes \
--advertise-routes=192.168.3.0/24 \
--login-server=https://headscale.example.com:8080 \
--auth-key=xxxxxxxxxxxx
--accept-routes
代表接受其他节点的路由指示。--advertise-routes
指定本节点对其他节点的路由建议,即哪个网段走VPN到本节点。一般是本节点的内网网段。--login-server
指定服务端的URL。--auth-key
指定在UI端生成的Preauth Key。
2. 打开UI端网页,进入“Device View”,把各节点的“Device Routes”设置为“active”,如下图。
这里还可以看到各个节点分配的VPN IP地址。
NAS配置(步骤4)
要在NAS上开启路由转发,把VPN路由过来的包转发到内网。
- 通过ssh登录到NAS,执行命令。
$ ip addr
2. 找到NAS的内网IP地址所对应的虚拟网卡名,我这里是ovs_eth0
;找到VPN地址所对应的网卡名,我这里是tailscale0
。
3. 执行命令:启用IPv4转发功能;防火墙配置了两个网络接口(ovs_eth0
和tailscale0
)的数据包转发规则,并执行网络地址转换(NAT)操作。使能了VPN子网和内网的双向互访。
$ sudo iptables -I FORWARD -i ovs_eth0 -j ACCEPT
$ sudo iptables -I FORWARD -o ovs_eth0 -j ACCEPT
$ sudo iptables -t nat -I POSTROUTING -o ovs_eth0 -j MASQUERADE
$ sudo iptables -I FORWARD -i tailscale0 -j ACCEPT
$ sudo iptables -I FORWARD -o tailscale0 -j ACCEPT
$ sudo iptables -t nat -I POSTROUTING -o tailscale0 -j MASQUERADE
$ sudo sysctl -w net.ipv4.ip_forward=1
4. 最后,把它们加到群晖的“计划任务”,开机触发启动。
- 去掉所有
sudo
,以root执行。 - 为了保证VPN相关的容器先启动,最上面最好加个
sleep 1m
。
主路由配置(步骤5)
为了让本节点内网的其他地址也能通过VPN访问其他节点的内网,需要在主路由上添加静态路由,例如下表。
描述 | 目的地址 | 子网掩码 | 下一跳地址 | 出接口 |
---|---|---|---|---|
访问VPN节点 | 100.100.0.0 | 255.255.0.0 | 本节点NAS地址 | LAN |
访问其他节点的内网 | 其他节点的内网网段 | 其他节点的内网掩码 | 本节点NAS地址 | LAN |
经过ping测试,大功告成!
贡献
本文介绍了docker-compose
配合lucky
反向代理实现Headscale
VPN快速私有化部署的方法、流程和大致原理。绝大多数操作都在UI网页端,将“低代码”进行到底。
免费的SD-WAN,唾手可得。